﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text.RegularExpressions;

namespace Assertion.NUnit
{
    /// <summary>
    /// NUnit test helper class.
    /// Traditionally NUnit stops test and marks it as failed on the first rule check failure. 
    /// But sometimes subsequent rules check may helps with bug location. 
    /// This class is designed to help in similar situations. 
    /// </summary>
    /// <example>This sample shows how to use this class.
    /// All you need is to relpase this code:
    /// <code>
    /// Assert.That(context.Containers.Count() == 3);
    /// Assert.That(context.Containers.Count(a => a.Name == "parent") == 1);
    /// Assert.That(context.Containers.Count(a => a.Name == "child") == 1);
    /// Assert.That(context.Containers.Count(a => a.Name == "son") == 1);
    /// </code>
    /// with this:
    /// <code>
    /// Assertion test = new Assertion(IsPrintable : true);
    /// test.Add(() => (context.Containers.Count() == 3));
    /// test.Add(() => (context.Containers.Count(a => a.Name == "parent") == 1));
    /// test.Add(() => (context.Containers.Count(a => a.Name == "child") == 1));
    /// test.Add(() => (context.Containers.Count(a => a.Name == "son") == 1));
    /// Assert.That(test);
    /// </code>
    /// </example>
    public class Assertion
    {
        /// <summary>
        /// Single check rule class
        /// </summary>
        public class Check
        {
            public Func<bool> Function { get; private set; }
            public string Name { get; private set; }

            private bool? checkResult = null;
            public bool IsSuccessful { get { return checkResult ?? DoCheck(); } }
            public bool IsFailed { get { return !checkResult ?? !DoCheck(); } }

            public Check(Func<bool> checkFunction, string checkName)
            {
                Function = checkFunction;
                Name = checkName;
            }

            public Check(Expression<Func<bool>> checkExpression, string checkName = null)
            {
                Function = checkExpression.Compile();
                Name = checkName ?? getExpressionCode(checkExpression);
            }

            public bool DoCheck()
            {
                checkResult = Function();
                return (bool)checkResult;
            }

            public override string ToString()
            {
                return (IsSuccessful) ? "Success" : "Failed ";
            }

            /// <summary>
            /// Gets printable expression name from Expression object
            /// </summary>
            /// <param name="expression">Expression object</param>
            /// <returns>printable expression name</returns>
            private string getExpressionCode(Expression<Func<bool>> expression)
            {
                //converts this string:
                //   (value(EFDataSync.TestDataSync+<>c__DisplayClass5).context.Containers.First(a => (a.Name == "parent")) 
                //         == value(EFDataSync.TestDataSync+<>c__DisplayClass5).father)
                //to this:
                // (context.Containers.First(a => (a.Name == "parent")) == father)

                string bodyText = expression.Body.ToString();
                string deletePattern = @"value\([\w\.]+\+\<\>[\w_]+\)\.";

                bodyText = (new Regex(deletePattern)).Replace(bodyText, "");
                return bodyText;
            }

            public class ComparerByName : IEqualityComparer<Check>
            {
                public bool Equals(Check x, Check y) { return x.Name == y.Name; }
                public int GetHashCode(Check obj) { return obj.Name.GetHashCode(); }
            }

            public class ComparerByNameAndResults : IEqualityComparer<Check>
            {
                public bool Equals(Check x, Check y) { return x.Name == y.Name && x.IsSuccessful == y.IsSuccessful; }
                public int GetHashCode(Check obj) { return (obj.Name + obj.IsSuccessful).GetHashCode(); }
            }
        }

        private List<Check> checkList;
        public bool IsPrintable;

        /// <summary>
        /// Constructir
        /// </summary>
        /// <param name="IsPrintable">if true - prints failures list on Console on conversion to bool. Default - false</param>
        public Assertion(bool IsPrintableOnConvertToBool = false)
        {
            checkList = new List<Check>();
            this.IsPrintable = IsPrintableOnConvertToBool;
        }

        /// <summary>
        /// rerun all checks
        /// </summary>
        public void ReCheck()
        {
            foreach (Check test in checkList)
                test.DoCheck();
        }

        /// <summary>
        /// Add Expression to check list
        /// </summary>
        /// <param name="checkExpression">check Expression</param>
        /// <param name="checkName">chack Name (optional)</param>
        public void Add(Expression<Func<bool>> checkExpression, string checkName = null)
        {
            Check newTest = new Check(checkExpression, checkName);
            if (checkList.Count(a => a.Name == newTest.Name) > 0)
                throw new InvalidOperationException("Check with this Name is already present");
            checkList.Add(newTest);
        }

        /// <summary>
        /// Add delegate to check list
        /// </summary>
        /// <param name="checkFunction">delegate</param>
        /// <param name="checkName">printable name</param>
        public void Add(Func<bool> checkFunction, string checkName)
        {
            if (checkList.Count(a => a.Name == checkName) > 0)
                throw new InvalidOperationException("Check with this Name is already present");
            checkList.Add(new Check(checkFunction, checkName));
        }

        /// <summary>
        /// Returns list of failed checks
        /// </summary>
        /// <returns>list of failed checks</returns>
        public List<Check> FailedChecks()
        {
            return (from a in checkList where a.IsFailed select a).ToList();
        }

        /// <summary>
        /// Returns list of successful checks
        /// </summary>
        /// <returns>list of successful checks</returns>
        public List<Check> SuccessfulChecks()
        {
            return (from a in checkList where a.IsSuccessful select a).ToList();
        }

        /// <summary>
        /// Returns list of checks
        /// </summary>
        /// <returns>list of checks</returns>
        public List<Check> AllChecks()
        {
            return (from a in checkList select a).ToList();
        }

        /// <summary>
        /// Returns pritable list of failed checks
        /// </summary>
        /// <returns>pritable list of failed checks</returns>
        public string ToStringShort()
        {
            if (checkList.Count() == 0)
                return "No checks in Assertion";

            string result = "";
            foreach (Check test in checkList)
                if (test.IsFailed)
                    result += String.Format("{0} {1}\n", test, test.Name);
            if (result == "")
                result = "No failed checks in Assertion";
            return result;
        }

        /// <summary>
        /// Returns pritable list of checks
        /// </summary>
        /// <returns>pritable list of checks</returns>
        public string ToStringFull()
        {
            if (checkList.Count() == 0)
                return "No checks in Assertion";

            string result = "";
            foreach (Check test in checkList)
                result += String.Format("{0} {1}\n", test, test.Name);
            return result;
        }

        /// <summary>
        /// Conversion to bool for object using in Assert.That() expression
        /// If IsPrintable is setted and there were failed checks their list printed on Console
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public static implicit operator bool(Assertion value)
        {
            if (value.IsPrintable)
                Console.Write(value.ToStringShort());

            return value.checkList.Count(a => a.IsFailed) == 0;
        }

        /// <summary>
        /// Comparision of Assertions. Assertions are identical by content if names of their checks are identical
        /// </summary>
        /// <param name="right">Assertion for comparision</param>
        /// <returns>Comparision result</returns>
        public bool IsIdenticalByContent(Assertion right)
        {
            return checkList.Except(right.checkList, new Check.ComparerByName()).Count() == 0;
        }

        /// <summary>
        /// Comparision of Assertions. Assertions are identical by content if names and results of their checks are identical
        /// </summary>
        /// <param name="right">Assertion for comparision</param>
        /// <returns>Comparision result</returns>
        public bool IsIdenticalByContentAndResults(Assertion right)
        {
            return checkList.Except(right.checkList, new Check.ComparerByNameAndResults()).Count() == 0;
        }

        /// <summary>
        /// Comparision of Assertions. Assertions are identical by content if names of their checks are identical
        /// </summary>
        /// <param name="left">Assertion for comparision</param>
        /// <param name="right">Assertion for comparision</param>
        /// <returns>Comparision result</returns>
        public static bool AreIdenticalByContent(Assertion left, Assertion right)
        {
            return left.IsIdenticalByContent(right);
        }

        /// <summary>
        /// Comparision of Assertions. Assertions are identical by content if names and results of their checks are identical
        /// </summary>
        /// <param name="left">Assertion for comparision</param>
        /// <param name="right">Assertion for comparision</param>
        /// <returns>Comparision result</returns>
        public static bool AreIdenticalByContentAndResults(Assertion left, Assertion right)
        {
            return left.IsIdenticalByContentAndResults(right);
        }

        /// <summary>
        /// Returns list of checks which names are present only in this Assertion
        /// </summary>
        /// <param name="right">Assertion for comparision</param>
        /// <returns>list of checks which names are present only only in this Assertion</returns>
        public List<Check> DiffByContentLeftOnly(Assertion right)
        {
            return (from leftItem in this.checkList
                   join rightItem in right.checkList on leftItem.Name equals rightItem.Name into leftm
                   from m in leftm.DefaultIfEmpty()
                   where m == null
                   select leftItem).ToList();
        }

        /// <summary>
        /// Returns list of checks which names are present only only in argument Assertion
        /// </summary>
        /// <param name="right">Assertion for comparision</param>
        /// <returns>list of checks which names are present only only in argument Assertion</returns>
        public List<Check> DiffByContentRightOnly(Assertion right)
        {
            return right.DiffByContentLeftOnly(this);
        }

        /// <summary>
        /// Returns list of checks which names and results are present only in this Assertion
        /// </summary>
        /// <param name="right">Assertion for comparision</param>
        /// <returns>list of checks which names and results are present only only in this Assertion</returns>
        public List<Check> DiffByContentAndResultsLeftOnly(Assertion right)
        {
            return (from leftItem in checkList
                    from rightItem in right.checkList.Where(rightItem =>
                                                            rightItem.Name == leftItem.Name &&
                                                            rightItem.IsSuccessful == leftItem.IsSuccessful).DefaultIfEmpty()
                    where rightItem == null
                    select leftItem).ToList();
        }

        /// <summary>
        /// Returns list of checks which names and results are present only only in argument Assertion
        /// </summary>
        /// <param name="right">Assertion for comparision</param>
        /// <returns>list of checks which names and results are present only only in argument Assertion</returns>
        public List<Check> DiffByContentAndResultsRightOnly(Assertion right)
        {
            return right.DiffByContentAndResultsLeftOnly(this);
        }
    }
}
